
package com.bangcommunity.bbframe.sdm.aop.wrapper;

import com.bangcommunity.bbframe.common.annotation.CacheDelete;
import com.bangcommunity.bbframe.common.annotation.CacheSave;
import com.bangcommunity.bbframe.common.utils.lang.StringUtils;
import com.bangcommunity.bbframe.dynconfig.lock.ZkLock;
import com.bangcommunity.bbframe.springredis.RedisClient;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 *
 * @author tanghc
 * @date 17/1/18
 */
public class CacheFirstAopServiceWrapper extends BaseAopWrapper {
    private Map<Method, List<Annotation>> annotationListMap = new ConcurrentHashMap<>();
    private Map<Method, String[]> parameterNamesMap = new ConcurrentHashMap<>();

    public CacheFirstAopServiceWrapper(AopWrapper nextWrapper) {
        super(nextWrapper);
    }

    @Override
    protected Object doNextWrapper(ProceedingJoinPoint pjp) throws Throwable {
        CacheSave cacheSave = null;
        CacheDelete cacheDelete = null;
        // 拦截的方法
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
        Object[] arguments = pjp.getArgs();
        Object thisObj = pjp.getThis();
        for (Annotation annotation : getAnnotation(pjp)) {
            if (cacheSave == null && annotation.annotationType().equals(CacheSave.class)) {
                cacheSave = (CacheSave) annotation;
            }
            if (cacheDelete == null && annotation.annotationType().equals(CacheDelete.class)) {
                cacheDelete = (CacheDelete) annotation;
            }
        }
        if (cacheDelete != null) {
            cacheDelete(thisObj, method, arguments, cacheDelete.key());
        }
        if (cacheSave != null) {
            int expireSecond = cacheSave.value();
            String cacheKey = getCacheKey(thisObj, method, arguments, cacheSave.key());
            if (StringUtils.isNotBlank(cacheKey)) {
                Object result = RedisClient.get(cacheKey);
                if (null != result) {
                    logger.info("hit cache {},{}", cacheKey, result);
                    if (result instanceof Collection) {
                        Collection collection = (Collection) result;
                        if (collection.size() > 0) {
                            return result;
                        }
                    } else if (result.getClass().isArray()) {
                        Object[] array = (Object[]) result;
                        if (array.length > 0) {
                            return result;
                        }
                    } else if (result instanceof Map) {
                        Map map = (Map) result;
                        if (map.size() > 0) {
                            return result;
                        }
                    } else if (result instanceof String) {
                        String string = (String) result;
                        if (StringUtils.isNotBlank(string)) {
                            return result;
                        }
                    } else {
                        return result;
                    }
                }
                logger.info("not hit cache load from db {}", cacheKey);
                boolean acquireLock = ZkLock.getInstance().acquireLock(cacheKey, 10 * 1000);
                try {
                    if (acquireLock) {
                        result = RedisClient.get(cacheKey);
                        if (null != result) {
                            return result;
                        }
                        result = super.doNextWrapper(pjp);
                        if (null != result) {
                            RedisClient.set(cacheKey, result, expireSecond, TimeUnit.SECONDS);
                        }
                        return result;
                    }
                } finally {
                    try {
                        ZkLock.getInstance().releaseLock(cacheKey);
                    } catch (Exception e) {
                        logger.error("release zklock key error: key={}", cacheKey, e);
                    }
                }
            }
        }
        return super.doNextWrapper(pjp);
    }

    private List<Annotation> getAnnotation(ProceedingJoinPoint pjp) {
        // 拦截的方法
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
        // 拦截的类名
        String className = pjp.getTarget().getClass().getSimpleName();
        List<Annotation> annotationList = annotationListMap.get(method);
        if (annotationList == null) {
            annotationList = new ArrayList<>();
            Class<?> targetClass = AopProxyUtils.ultimateTargetClass(pjp.getThis());
            if (targetClass == null && pjp.getThis() != null) {
                targetClass = pjp.getThis().getClass();
            }
            Method targetMethod = null;
            try {
                targetMethod = targetClass.getMethod(method.getName(), method.getParameterTypes());
            } catch (NoSuchMethodException e) {
                logger.warn("no such method {}", e.getMessage());
            }
            annotationList.addAll(Arrays.asList(method.getAnnotations()));
            annotationList.addAll(Arrays.asList(method.getDeclaringClass().getAnnotations()));
            annotationList.addAll(Arrays.asList(targetMethod.getAnnotations()));
            annotationList.addAll(Arrays.asList(targetMethod.getDeclaringClass().getAnnotations()));
            annotationListMap.put(method, annotationList);
        }
        return annotationList;
    }

    private String parseCacheKeys(Object thisObj, Method method, String[] parameterNames, Object[] arguments,
            String key) {
        if (StringUtils.isNotBlank(key)) {
            String content = key;
            EvaluationContext ec = new StandardEvaluationContext();
            for (int i = 0; i < parameterNames.length; i++) {
                ec.setVariable(parameterNames[i], arguments[i]);
            }
            content = parser.parseExpression(content).getValue(ec, String.class);
            if (!content.equals(key)) {
                key = content;
            } else {
                key = key + "." + argsHashCode(arguments);
            }
            return key;
        } else {
            return thisObj.getClass().getName() + "." + method.getName() + "." + argsHashCode(arguments);
        }
    }

    private String getCacheKey(Object thisObj, Method method, Object[] arguments, String key) {
        String[] parameterNames = getParameterNames(method);
        return parseCacheKeys(thisObj, method, parameterNames, arguments, key);
    }

    private List<String> getCacheKeys(Object thisObj, Method method, Object[] arguments, String[] keys) {
        List<String> cacheKeys = new ArrayList<>();
        if (keys != null && keys.length > 0) {
            String[] parameterNames = getParameterNames(method);
            for (String key : keys) {
                String s = parseCacheKeys(thisObj, method, parameterNames, arguments, key);
                if (StringUtils.isNotBlank(s)) {
                    cacheKeys.add(s);
                }
            }
        } else {
            cacheKeys.add(thisObj.getClass().getName() + "." + method.getName() + "." + argsHashCode(arguments));
        }
        return cacheKeys;
    }

    private DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();

    private String[] getParameterNames(Method method) {
        String[] parameterNames = parameterNamesMap.get(method);
        if (parameterNames == null) {
            parameterNames = discoverer.getParameterNames(method);
            parameterNamesMap.put(method, parameterNames);
        }
        return parameterNames;
    }

    private ExpressionParser parser = new SpelExpressionParser();

    private void cacheDelete(Object thisObj, Method method, Object[] arguments, String[] keys) {
        List<String> cacheKeys = getCacheKeys(thisObj, method, arguments, keys);
        if (null != cacheKeys) {
            for (String key : cacheKeys) {
                RedisClient.del(key);
            }
        }
    }

    private static String argsHashCode(Object[] arguments) {
        if (null == arguments || arguments.length == 0) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        for (Object object : arguments) {
            if (null != object) {
                if (sb.length() > 0) {
                    sb.append("|");
                }
                sb.append(object.toString());
            }
        }
        return sb.toString();
    }

}
